Skip to main content
  1. « Go back to Articles

Type-safe reversal of a record's key-value pairs

Image of the PropsForStyling type code

Suppose we have a static Record object like this:

const fruitCodes: Record<number, string> = {
  1: 'apple',
  2: 'banana',
  3: 'orange',
  // ...
} as const

Our goal is to implement a type-safe function reverseRecord() that produces the following output (including the mouseover type hint shown as comment below):

const reversedFruitCodes = reverseRecord(fruitCodes)
//    ^
//    const reversedFruitCodes: {
//      apple: 1,
//      banana: 2,
//      orange: 3,
//      ...
//    }

It’s surprisingly challenging to declare the return type of reverseRecord() correctly, and that’s what this article is about.

Implementation #

However, let’s begin with the functional implementation without types, as there are a number of ways to implement this function. Here, we’ll use a combination of the Object.entries() and Object.fromEntries() methods as follows:

const reverseRecord = (obj) =>
  Object.fromEntries(Object.entries(obj).map(([key, value]) => [value, key]))

This implementation is concise and the flipping of the object’s key-value pairs is done explicitly, which makes it (in my opinion) easy to understand. Unfortunately, the static methods on Object we used above are untyped, so we have to add a type for the function signature on our own.

Argument type #

Valid keys for a JavaScript records must be of type string | number | symbol , so in order to reverse the record with it’s values becoming it’s keys, we need to require that the values have one of these types as well. We can express this requirement for the obj parameter as generic type T extends Record<keyof T, string | number | symbol> and use it as follows:

const reverseRecord = <T extends Record<keyof T, string | number | symbol>>(obj: T) => /** implementation as above */

Return type #

The real challenge here is defining the return type of this function. It should accept T from above as input type parameter, and return the reversed record:

type ReversedRecord<T extends Record<keyof T, string | number | symbol>> = /** ??? */

Let’s go through the implementation step by step.

First, we want to return a record with the values of T as keys:

type ReversedRecord<T extends Record<keyof T, string | number | symbol>> = {
  [P in T[keyof T]]: P // used here to demonstrate the value of P
}

type ReversedFruitCodes = ReverseRecord<typeof fruitCodes>
//   ^
//   {
//     apple: "apple";
//     banana: "banana";
//     orange: "orange";
//   }

We defined a mapped type that iterate the union of T’s values (obtained from the unusual combination of the union of it’s keys created via keyof T = "apple" | "banana" | "orange" with the indexed access type T[keyof T]) to create the keys of ReversedRecord.

Second, instead of P, we need the original key K of T that corresponds to P, or in other words, we need to find a way to define a type variable K such that T[K] == P.

This can only be done by a trick that is very confusing at first: we begin by declaring an object with many keys K in keyof T instead of P in the type from above:

type ReversedRecord<T extends Record<keyof T, string | number | symbol>> = {
  [P in T[keyof T]]: {
    [K in keyof T]: K | T[K] // used here to demonstrate the values of K and T[K]
  }
}

type ReversedFruitCodes = ReverseRecord<typeof fruitCodes>
//   ^
//   {
//     apple: {
//       readonly 1: 1 | "apple";
//       readonly 2: 2 | "banana";
//       readonly 3: 3 | "orange";
//     };
//     /** etc. */
//   }

Third, remember that in this inner object type, P has a type of e.g. “apple” for all K in keyof T, so we can do a comparison using conditional type to identify the key we want (e.g. K == 1 for P == "apple") and discard the others by setting them to never:

type ReversedRecord<T extends Record<keyof T, string | number | symbol>> = {
  [P in T[keyof T]]: {
    [K in keyof T]: T[K] extends P ? K : never
  }
}

type ReversedFruitCodes = ReverseRecord<typeof fruitCodes>
//   ^
//   {
//     apple: {
//       readonly 1: "apple";
//       readonly 2: never;
//       readonly 3: never;
//     };
//     /** etc. */
//   }

Last, we can pick the property that is not never by utilizing the indexed access type

{ readonly 1: "apple"; readonly 2: never; readonly 3: never; }[keyof T]
// = 1

because keyof T = "apple" | "banana" | "orange" and TypeScript discards type values that are never. Putting it all together, our end result reads:

type ReversedRecord<T extends Record<keyof T, string | number | symbol>> = {
  [P in T[keyof T]]: {
    [K in keyof T]: T[K] extends P ? K : never
  }[keyof T]
}

type ReversedFruitCodes = ReverseRecord<typeof fruitCodes>
//   ^
//   {
//     apple: 1;
//     banana: 2;
//     orange: 3;
//   }

This is an excellent example of the very different way of thinking that’s required to construct certain kinds of types: Sometimes, you need to build a large and complex object type first to get the value you need, only to afterwards finding a way to narrow the object type down again to just the required value.

The finished reverseRecord() implementation #

We can now use the ReversedRecord type in our final implementation of reverseRecord():

const reverseRecord = <T extends Record<keyof T, string | number | symbol>>(
  obj: T,
): ReversedRecord<T> =>
  Object.fromEntries(Object.entries(obj).map(([key, value]) => [value, key]))

There is one last caveat, however. While the return value of the function is completely type-safe to use, the mouseover tooltip is not what we expected because TypeScript seems not to evaluate the complete expression and we end up with this:

const reversedFruitCodes = reverseRecord(fruitCodes)
//    ^
//    const reversedFruitCodes: ReverseRecord<{
//      readonly 1: "apple";
//      readonly 2: "banana";
//      readonly 3: "orange";
//    }>

We can force TypeScript to do so by using the Expand type defined the my other article “Forcing IntelliSense to resolve a type definition” and declaring Expand<ReversedRecord<T>> as return type of the reverseRecord() function:

const reversedFruitCodes = reverseRecord(fruitCodes)
//    ^
//    const reversedFruitCodes: {
//      "apple": 1;
//      "banana": 2;
//      "orange": 3;
//    }

Usage examples #

The reverseRecord() function is useful for large datasets where reverse lookups need to be done very often. Iterating all entries in the object to find a key by value corresponds to performing a linear search with an O(n) time complexity, which is something we may want to avoid. Instead, we generate the inverse lookup table only once by reversing the original record, and can subsequently access the key by value as indexed access in constant O(1) time.

Another example for leveraging the type-safe return value would be different implementation of the HttpError class (introduced in my article Implementing an HTTP error class in TypeScript) that would be instanciated with the error message instead of status code:

const httpErrorCodes = reverseRecord(httpErrorMessages)

/** The implementation is left as an exercise to the reader ;-) */

throw new HttpError({ message: 'Unauthorized' })
//    ^
//    HttpError<401, "Unauthorized">
Dr. Ole Hüter
Author
Dr. Ole Hüter
Freelance Full Stack Web Developer